/**
 * 
 */
package truerefactor.refactor;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import truerefactor.graph.Accessibility;
import truerefactor.graph.AttributeNode;
import truerefactor.graph.ClassNode;
import truerefactor.graph.CodeGraph;
import truerefactor.graph.CodeNode;
import truerefactor.graph.Edge;
import truerefactor.graph.EdgeType;
import truerefactor.graph.MethodNode;

/**
 * MoveMethod - An implementation of the MoveMethod refactoring technique
 * 
 * @author Isaac Griffith
 */
public class MoveMethod extends Refactoring {

    /**
     * Creates a new instance of MoveMethod
     */
    public MoveMethod()
    {
        this.name = "Move Method";
        this.description = "";
    }

    /*
     * (non-Javadoc)
     * @see
     * truerefactor.refactor.Refactoring#execute(truerefactor.graph.CodeNode [])
     */
    @Override
    public void execute(CodeGraph graph)
    {
        // Assume that the first CodeNode is the method to be moved, and the
        // second is the class to move it to
        if (!(operands.get(0) instanceof MethodNode))
            return;

        MethodNode sourceMethod = (MethodNode) operands.get(0);
        ClassNode source = sourceMethod.getParentClass();
        ClassNode target = (ClassNode) operands.get(1);

        if (source == null || target == null)
            return;

        if (source.equals(target))
            return;

        List<AttributeNode> assocFields = sourceMethod.getAssocAttrs();
        List<MethodNode> assocMethods = sourceMethod.getAssociatedMethods();
        List<MethodNode> methodsToMove = new ArrayList<MethodNode>();
        List<AttributeNode> attrsToMove = new ArrayList<AttributeNode>();
        int biDirectRef = 0;

        biDirectRef = examineFeatures(sourceMethod, target, assocFields, assocMethods, methodsToMove, attrsToMove,
                biDirectRef);

        if (biDirectRef == -1)
            return;

        if (!findDeclarations(sourceMethod, source))
            return;

        // 3. Declare the method in the target class
        // a. You may choose to use a different name, one that makes sense in
        // the target class
        sourceMethod.setParentClass(target);
        graph.insert(new Edge(target, sourceMethod, EdgeType.Method));
        target.addMethod(sourceMethod);

        // FIXME: need to adjust all the associated information dealing with the
        // moved method

        adjustReferences(graph, source, target);

        // 6. Turn the source method into a delegating method.
        // MethodNode delegate = new MethodNode(sourceMethod.getIdentifier(),
        // "", Accessibility.Public);
        // graph.insert(delegate);
        // graph.insert(new Edge(source, delegate, EdgeType.Method));
        // delegate.setParentClass(source);
        // graph.insert(new Edge(delegate, sourceMethod,
        // EdgeType.UseDependency));
        // sourceMethod.addOtherMethod(delegate);

        // 7. Decide whether to remove the source method or retain it as a
        // delegating method.
        // a. leaving the source as a delegating method is easier if you have
        // many references
        // completed by moving original method. TODO must be fixed for code
        // generation
        source.removeMethod(sourceMethod);
        // graph.remove(graph.getEdge(source, sourceMethod, EdgeType.Method));

        replaceReferences(graph, sourceMethod, source, target, methodsToMove, attrsToMove, biDirectRef);
    }

    private boolean findDeclarations(MethodNode sourceMethod, ClassNode source)
    {
        // 2. Check the sub- and superclasses of the source class for other
        // declarations of the method.
        // a. if there are any other declarations, you may not be able to make
        // the move, unless polymorphism can also be expressed on the target
        List<ClassNode> parents = source.getParentClasses();
        List<ClassNode> subClasses = source.getSubClasses();

        for (ClassNode parent : parents)
        {
            for (CodeNode node : parent.getMethods())
            {
                if (node instanceof MethodNode)
                {
                    MethodNode other = (MethodNode) node;
                    String srcName = sourceMethod.getIdentifier().substring(
                            sourceMethod.getIdentifier().lastIndexOf(".") + 1);
                    String nodeName = node.getIdentifier().substring(node.getIdentifier().lastIndexOf(".") + 1);

                    if (srcName.equals(nodeName))
                    {
                        if (sourceMethod.getParameters().size() == other.getParameters().size())
                        {
                            return false;
                        }
                    }
                }
            }
        }

        for (ClassNode subclass : subClasses)
        {
            for (CodeNode node : subclass.getMethods())
            {
                if (node instanceof MethodNode)
                {
                    MethodNode other = (MethodNode) node;
                    String srcName = sourceMethod.getIdentifier().substring(
                            sourceMethod.getIdentifier().lastIndexOf(".") + 1);
                    String nodeName = node.getIdentifier().substring(node.getIdentifier().lastIndexOf(".") + 1);

                    if (srcName.equals(nodeName))
                    {
                        if (sourceMethod.getParameters().size() == other.getParameters().size())
                        {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    private int examineFeatures(MethodNode sourceMethod, ClassNode target, List<AttributeNode> assocFields,
            List<MethodNode> assocMethods, List<MethodNode> methodsToMove, List<AttributeNode> attrsToMove,
            int biDirectRef)
    {
        // 1. Examine all features used by the source method that are defined on
        // the source class. Consider whether they also should be moved.
        // a. If a feature is used only by the method you are about to move, you
        // might as well move it, too. IF the feature is used by other methods,
        // consider moving them as well. Sometimes it is easier to move a clutch
        // of methods than to move them one at a time.

        for (AttributeNode attr : assocFields)
        {
            if (attr.getAssociatedMethods().size() <= 1)
            {
                attrsToMove.add(attr);
            }
            else
            {
                biDirectRef = 1;
            }
        }

        for (MethodNode method : assocMethods)
        {
            if (method.getAssociatedMethods().size() <= 1)
            {
                methodsToMove.add(method);
            }
            else
            {
                biDirectRef = 1;
            }
        }

        for (CodeNode node : target.getMethods())
        {
            if (node instanceof MethodNode)
            {
                MethodNode other = (MethodNode) node;
                String srcName = sourceMethod.getIdentifier().substring(
                        sourceMethod.getIdentifier().lastIndexOf(".") + 1);
                String nodeName = node.getIdentifier().substring(node.getIdentifier().lastIndexOf(".") + 1);

                if (srcName.equals(nodeName))
                {
                    if (sourceMethod.getParameters().size() == other.getParameters().size())
                    {
                        return -1;
                    }
                }
            }
        }
        return biDirectRef;
    }

    private void adjustReferences(CodeGraph graph, ClassNode source, ClassNode target)
    {
        // 5. Determine how to reference the correct target object from the
        // source
        // a. There may be an existing field or method that will give you the
        // target. IF not, see whether you can easily create a method that will
        // do so. Failing that, you need to create a new field in the source
        // that can store the target. This may be a permanent change, but you
        // can also make it temporary until you have refactored enough to remove
        // it.
        if (!source.getMethodsByReturnType(target.getIdentifier()).isEmpty())
        {
            // TODO modify the code, will be implemented when code generation is
            // implemented
        }
        else if (source.getAttributeByType(target.getIdentifier()) != null)
        {
            // TODO modify the code, will be implemented when code generation is
            // implemented
        }
        else
        {
            AttributeNode aNode = new AttributeNode(source, "ref" + target.getIdentifier(), "");
            aNode.setAccessibility(Accessibility.Private);
            aNode.setType(target.getIdentifier());
            aNode.setAssociatedClass(target);
            aNode.setParent(source);
            source.addAttribute(aNode);

            graph.insert(aNode);
            graph.insert(new Edge(source, aNode, EdgeType.Attribute));
        }
    }

    private void replaceReferences(CodeGraph graph, MethodNode sourceMethod, ClassNode source, ClassNode target,
            List<MethodNode> methodsToMove, List<AttributeNode> attrsToMove, int biDirectRef)
    {
        // 8. IF you remove the source method, replace all the references with
        // references to the target method.
        // a. You can compile and test after changing each reference, although
        // it is usually easier to change all references with one search and
        // replace.
        // completed by moving original method. TODO must be fixed for code
        // generation
        boolean addRef = false;
        for (CodeNode node : source.getMethods())
        {
            if (node instanceof MethodNode)
            {
                MethodNode method = (MethodNode) node;
                if (method.getAssociatedMethods().contains(sourceMethod))
                {
                    addRef = true;
                    method.removeAssociatedMethod(sourceMethod);
                    method.addOtherMethod(sourceMethod);
                }
            }
        }

        for (CodeNode node : source.getAttributes())
        {
            if (node instanceof AttributeNode)
            {
                AttributeNode attr = (AttributeNode) node;
                if (attr.getAssociatedMethods().contains(sourceMethod))
                {
                    addRef = true;
                    attr.removeAssociatedClassMethod(sourceMethod);
                    attr.addAssociatedOtherMethod(sourceMethod);
                }
            }
        }

        if (addRef)
        {
            String type = target.getIdentifier();
            String attrName = "a" + type.substring(0, 1).toUpperCase() + type.substring(1);
            AttributeNode node = new AttributeNode(source, attrName, "");
            node.setType(type);
            node.setPrimitive(false);
            node.setFinal(false);
            node.setStatic(false);
            node.setAssociatedClass(target);

            graph.insert(node);
            graph.insert(new Edge(source, node, EdgeType.Attribute));
            source.addAttribute(node);
            if (!graph.isEdge(source, target, EdgeType.Association))
            {
                graph.insert(new Edge(source, target, EdgeType.Association));
                source.addAssociatedClass(target);
            }
        }

        if (biDirectRef > 0)
        {
            String type = source.getIdentifier();
            String attrName = "a" + type.substring(0, 1).toUpperCase() + type.substring(1);
            AttributeNode node = new AttributeNode(source, attrName, "");
            node.setType(type);
            node.setPrimitive(false);
            node.setFinal(false);
            node.setStatic(false);
            node.setAssociatedClass(source);

            graph.insert(node);
            graph.insert(new Edge(target, node, EdgeType.Attribute));
            target.addAttribute(node);

            if (!graph.isEdge(target, source, EdgeType.Association))
            {
                graph.insert(new Edge(target, source, EdgeType.Association));
                target.addAssociatedClass(source);
            }
        }

        List<MethodNode> newAssocMethods = new ArrayList<MethodNode>();
        List<MethodNode> newOtherMethods = new ArrayList<MethodNode>();
        List<AttributeNode> newAssocFields = new ArrayList<AttributeNode>();
        List<AttributeNode> newOtherFields = new ArrayList<AttributeNode>();

        List<Refactoring> refactorings = new ArrayList<Refactoring>();
        for (AttributeNode toMove : attrsToMove)
        {
            refactorings.add(RefactoringFactory.getInstance().createMoveField(toMove, target));
        }

        for (MethodNode toMove : methodsToMove)
        {
            refactorings.add(RefactoringFactory.getInstance().createMoveMethod(toMove, target));
        }

        for (Refactoring refactor : refactorings)
        {
            refactor.execute(graph);
        }

        for (MethodNode assoc : sourceMethod.getAssociatedMethods())
        {
            if (!assoc.getParentClass().equals(sourceMethod.getParentClass()))
            {
                newOtherMethods.add(assoc);
                assoc.removeAssociatedMethod(sourceMethod);
                assoc.addOtherMethod(sourceMethod);
            }
            else
            {
                newAssocMethods.add(assoc);
            }
        }

        for (MethodNode other : sourceMethod.getOtherMethods())
        {
            if (!other.getParentClass().equals(sourceMethod.getParentClass()))
            {
                newOtherMethods.add(other);
            }
            else
            {
                newAssocMethods.add(other);
                other.removeOtherMethod(sourceMethod);
                other.addAssociatedMethod(sourceMethod);
            }
        }

        for (AttributeNode assoc : sourceMethod.getAssocAttrs())
        {
            if (!assoc.getParent().equals(sourceMethod.getParentClass()))
            {
                newOtherFields.add(assoc);
                assoc.addAssociatedOtherMethod(sourceMethod);
                assoc.removeAssociatedClassMethod(sourceMethod);
            }
            else
            {
                newAssocFields.add(assoc);
            }
        }

        for (AttributeNode other : sourceMethod.getOtherAttributes())
        {
            if (!other.getParent().equals(sourceMethod.getParentClass()))
            {
                newOtherFields.add(other);
            }
            else
            {
                newAssocFields.add(other);
                other.removeAssociatedOtherMethod(sourceMethod);
                other.addAssociatedClassMethod(sourceMethod);
            }
        }

        sourceMethod.setOtherMethods(newOtherMethods);
        sourceMethod.setAssocMethods(newAssocMethods);
        sourceMethod.setOtherAttrs(newOtherFields);
        sourceMethod.setAssocAttrs(newAssocFields);
    }
}
